/*******************************************************************************
 *                                                                             *
 *  IrcStream.cs                                                               *
 *                                                                             *
 *  IrcDotNet - Internet Relay Chat API for .NET and Mono                      *
 *  << http://code.google.com/p/ircdotnet >>                                   *
 *                                                                             *
 *  Copyright (C) 2006 Rodger Aiken (johnny.mn3m0nic@gmail.com)                *
 *                                                                             *
 *  IRC .NET is free software; you can redistribute it and/or modify it under  *
 *  the terms of the GNU Lesser General Public License as published by the     *
 *  Free Software Foundation; either version 2.1 of the License, or (at	your   *
 *  option) any later version.                                                 *
 *                                                                             *
 *  IRC .NET is distributed in the hope that it will be useful, but WITHOUT    *
 *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or      *
 *  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public        *
 *  License for more details.                                                  *
 *                                                                             *
 *  You should have received a copy of the GNU Lesser General Public License   *
 *  along with this library; if not, write to the Free Software Foundation,    *
 *  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA           *
 *                                                                             *
 *******************************************************************************/

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace IrcDotNet {
	public class IrcStream : Stream {
		protected Socket socket = null;
		private StringBuilder localBuffer = null;

		private bool autoCorrect = false;

		/// <summary>
		/// If this property is set to true, then all strings are checked
		/// for IRC protocol compliance, and corrected before being written
		/// to the underlying socket.
		/// </summary>
		public bool AutoCorrect {
			get { return this.autoCorrect; }
			set { this.autoCorrect = value; }
		}

		/// <summary>
		/// Gets a value indicating whether or not the stream is readable.
		/// Since IrcStream performs all I/O operations directly on the socket
		/// this value is always true.
		/// </summary>
		public override bool CanRead { get { return true; } }

		/// <summary>
		/// Gets a value indicating whether or not the stream supports seeking.
		/// Since IrcStream is not buffered, and is a network connection
		/// this value is always false.
		/// </summary>
		public override bool CanSeek { get { return false; } }

		/// <summary>
		/// Gets a value indicating whether or not the stream support timeout;
		/// this value is always true.
		/// </summary>
		public override bool CanTimeout { get { return true; } }

		/// <summary>
		/// Gets a value indicating whether or not the stream is writable.
		/// Since IrcStream performs all I/O operations directly on the socket
		/// this value is always true.
		/// </summary>
		public override bool CanWrite { get { return true; } }

		/// <summary>
		/// Gets a value indicating whether or not the underlying socket
		/// is connected.
		/// </summary>
		public bool Connected { get { return this.socket.Connected; } }

		private Encoding encoding = Encoding.ASCII;

		/// <summary>
		/// Determines the encoding used for strings sent / received
		/// over the stream.
		/// </summary>
		public Encoding Encoding {
			get { return this.encoding; }
			set { this.encoding = value; }
		}

		/// <summary>
		/// Always throws NotSupportedException.
		/// </summary>
		public override long Length {
			get {
				throw new NotSupportedException("The Stream.Length property is implemented but not supported by this class");
				return -1;
			}
		}

		/// <summary>
		/// Always throws NotSupportedException.
		/// </summary>
		public override long Position {
			get {
				throw new NotSupportedException("The Stream.Position property is implemented but not supported by this class");
				return -1;
			}
		}

		public IrcStream ( Socket socket ) {
			this.socket = socket;
			this.socket.NoDelay = true;
			this.socket.ReceiveBufferSize = 4096;
			this.socket.SendBufferSize = 4096;

			this.socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
		}
		public IrcStream ( Socket socket, Encoding encoding ) {
			this.socket = socket;
			this.socket.NoDelay = true;
			this.socket.ReceiveBufferSize = 4096;
			this.socket.SendBufferSize = 4096;
			this.encoding = encoding;

			this.socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
		}
		public IrcStream ( string host ) {
			this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

			this.socket.Connect(new IPEndPoint(( Dns.GetHostEntry(host) ).AddressList[0], 6667));
			this.socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);

			this.socket.NoDelay = true;
			this.socket.ReceiveBufferSize = 4096;
			this.socket.SendBufferSize = 4096;
		}
		public IrcStream ( string host, int port ) {
			this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

			this.socket.Connect(new IPEndPoint(( Dns.GetHostEntry(host) ).AddressList[0], port));
			this.socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);

			this.socket.NoDelay = true;
			this.socket.ReceiveBufferSize = 4096;
			this.socket.SendBufferSize = 4096;
		}

		/// <summary>
		/// Validates and corrects the string being written to
		/// the stream.
		/// </summary>
		/// <param name="value">string to </param>
		public void CorrectWrite ( string value ) {
			if ( value == null )
				throw new ArgumentNullException("value");

			if ( value.IndexOf("\r\n") == -1 )
				throw new ArgumentException("value is not a valid IRC message.");

			string[ ] strings = ( value + " " ).Split(new string[ ] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);

			for ( int i = 0 ; i < strings.Length ; i++ ) {
				string buffer = strings[i];

				if ( buffer.Length <= 512 )
					this.Write(value);

				if ( !buffer.Contains(" :") )
					throw new ArgumentException("The length of value exceeds 512 characters");

				string leading = buffer.Substring(0, ( value.IndexOf(" :") + 2 ));
				string trailing = buffer.SubString(value.IndexOf(" :") + 2);

				int blockSize = ( buffer.Length - leading.Length );

				for ( int j = 0 ; j < trailing.Length ; j += blockSize ) {
					if ( ( j + blockSize ) > ( trailing.Length - ( j + blockSize ) ) ) {
						this.WriteLine(leading + trailing.Substring(j));
						break;
					}
					else
						this.WriteLine(leading + trailing.Substring(j, ( j + blockSize )));
				}
			}
		}

		/// <summary>
		/// Flushes data from the stream. This method is reserved for future use.
		/// </summary>
		public override void Flush ( ) { return; }

		/// <summary>
		/// Reads data from the <see cref="IrcDotNet.IrcStream"/>
		/// </summary>
		/// <param name="buffer">The location in buffer to begin storing the data to.</param>
		/// <param name="offset">The number of bytes to read from the <see cref="IrcDotNet.IrcStream"/></param>
		/// <param name="count">An array of type System.Byte that is the location in memory to store data read from the <see cref="IrcDotNet.IrcStream"/></param>
		/// <returns>The number of bytes read from the <see cref="IrcDotNet.IrcStream"/></returns>
		public override int Read ( byte[ ] buffer, int offset, int count ) {
			if ( this.disposed )
				throw new ObjectDisposedException();

			if ( buffer == null )
				throw new ArgumentNullException("buffer");

			if ( ( offset < 0 ) || ( offset > buffer.Length ) )
				throw new ArgumentOutOfRangeException("offset");

			if ( ( count < 0 ) || ( count > buffer.Length ) )
				throw new ArgumentOutOfRangeException("count");

			if ( ( offset + count ) > buffer.Length )
				throw new ArgumentException("The sum of offset and count is larger than the buffer length");

			int ret = -1;

			try {
				ret = this.socket.Receive(buffer, offset, count, SocketFlags.None);
			}
			catch ( SocketException socketException ) {
				throw new IOException(
					"An I/O error occurred while performing the requested operation.",
					socketException
				);
			}

			return ret;
		}
		/// <summary>
		/// Reads data from the <see cref="IrcDotNet.IrcStream"/>
		/// </summary>
		/// <param name="buffer">The buffer to begin storing the data to.</param>
		/// <returns>The number of bytes read from the <see cref="IrcDotNet.IrcStream"/></returns>
		public int Read ( byte[ ] buffer ) {
			return this.Read(buffer, 0, buffer.Length);
		}

		/// <summary>
		/// Reads a line of characters from the current stream and returns the data as a string.
		/// </summary>
		/// <param name="blocking">
		/// A value indicating wthere or not this method should block the current thread of execution.
		/// If true, this method blocks until data is available to be read.
		/// If false, then this method return immediately.
		/// </param>
		/// <returns>The next line from the input stream, or null if the end of the input stream is reached.</returns>
		public string ReadLine ( bool blocking ) {
			if ( this.localBuffer == null )
				this.localBuffer = new StringBuilder();

			byte[ ] buffer = new byte[4096];
			int bytesRead, totalBytesRead;

			do {
				bytesRead = this.Read(buffer);
				totalBytesRead += bytesRead;

				Decoder decoder = this.encoding.GetDecoder();
				char[ ] charBuffer = new char[decoder.GetCharCount(buffer, 0, bytesRead)];

				decoder.GetChars(buffer, 0, bytesRead, charBuffer, 0);
				this.localBuffer.Append(charBuffer);

				if ( ( this.localBuffer.ToString() ).IndexOf("\r\n") != -1 )
					break;
			} while ( ( bytesRead > 0 ) && ( totalBytesRead < buffer.Length ) );

			string data = this.localBuffer.ToString();

			if ( ( data.Length > 0 ) && ( data.IndexOf("\r\n") != -1 ) ) {
				string ret = data.Substring(0, data.IndexOf("\r\n"));

				if ( ret.Length < 1 )
					return ( blocking ? this.ReadLine(true) : null );

				this.localBuffer.Remove(0, data.IndexOf("\r\n"));
				return ret;
			}

			return ( blocking ? this.ReadLine(true) : null );
		}
		/// <summary>
		/// Reads a line of characters from the current stream and returns the data as a string.
		/// </summary>
		/// <returns>The next line from the input stream, or null if the end of the input stream is reached.</returns>
		public string ReadLine ( ) {
			return this.ReadLine(false);
		}


		/// <summary>
		/// Reads a line of characters from the current stream and returns the data as a <see cref="IrcDotNet.IrcMessage"/> object.
		/// </summary>
		/// <param name="blocking">
		/// A value indicating wthere or not this method should block the current thread of execution.
		/// If true, this method blocks until data is available to be read.
		/// If false, then this method return immediately.
		/// </param>
		/// <returns>The next <see cref="IrcDotNet.IrcMessage"/> from the input stream, or null if the end of the input stream is reached.</returns>
		public IrcMessage ReadMessage ( bool blocking ) {
			string buffer = this.ReadLine(blocking);
			return ( ( buffer != null ) ? IrcMessage.Parse(buffer) : null );
		}
		/// <summary>
		/// Reads a line of characters from the current stream and returns the data as a <see cref="IrcDotNet.IrcMessage"/> object.
		/// </summary>
		/// <returns>The next <see cref="IrcDotNet.IrcMessage"/> from the input stream, or null if the end of the input stream is reached.</returns>
		public IrcMessage ReadMessage ( ) {
			return this.ReadMessage(true);
		}

		/// <summary>
		/// Sets the current position of the stream to the given value.
		/// This method is not currently supported and always throws a <see cref="System.NotSupportedException" />.
		/// </summary>
		/// <param name="offset">This parameter is not used.</param>
		/// <param name="origin">This parameter is not used.</param>
		/// <returns>The position in the stream.</returns>
		public override long Seek ( long offset, SeekOrigin origin ) {
			throw new NotSupportedException(
				"The Stream.Seek method is implemented but not supported by this class"
			);
			return -1;
		}

		/// <summary>
		/// Sets the length of the stream. This method always throws a <see cref="System.NotSupportedException" />.
		/// </summary>
		/// <param name="value">This parameter is not used.</param>
		public override void SetLength ( long value ) {
			throw new NotSupportedException(
				"The Stream.SetLength method is implemented but not supported by this class"
			);
		}

		/// <summary>
		/// Returns a synchronized (thread-safe) wrapper of
		/// the IRC stream. (Wrapper for the Stream.Synchronized method.)
		/// </summary>
		/// <param name="ircStream">Stream to synchronize</param>
		/// <returns>Synchronized wrapper for the stream</returns>
		public static IrcStream Synchronized ( IrcStream ircStream ) {
			return ( ( IrcStream ) Stream.Synchronized(ircStream) );
		}

		/// <summary cref="Write">
		/// Writes data to the <see cref="IrcDotNet.IrcStream"/>
		/// </summary>
		/// <param name="buffer">The location in buffer from which to start writing data.</param>
		/// <param name="offset">The number of bytes to write to the <see cref="IrcDotNet.IrcStream"/>.</param>
		/// <param name="count"> An array of type System.Byte that contains the data to write to the <see cref="IrcDotNet.IrcStream"/>.</param>
		public override void Write ( byte[ ] buffer, int offset, int count ) {
			if ( this.disposed )
				throw new ObjectDisposedException();

			if ( buffer == null )
				throw new ArgumentNullException("buffer");

			if ( ( offset < 0 ) || ( offset > buffer.Length ) )
				throw new ArgumentOutOfRangeException("offset");

			if ( ( count < 0 ) || ( count > buffer.Length ) )
				throw new ArgumentOutOfRangeException("count");

			if ( ( offset + count ) > buffer.Length )
				throw new ArgumentException("The sum of offset and count is larger than the buffer length");

			try {
				this.socket.Send(buffer, offset, count, SocketFlags.None);
			}
			catch ( SocketException socketException ) {
				throw new IOException(
					"An I/O error occurred while performing the requested operation.",
					socketException
				);
			}
		}
		/// <summary>
		/// Writes data to the <see cref="IrcDotNet.IrcStream"/>
		/// </summary>
		/// <param name="buffer">The location in buffer from which to start writing data.</param>
		public void Write ( byte[ ] buffer ) {
			this.Write(buffer, 0, buffer.Length);
		}
		/// <summary>
		/// Writes data to the <see cref="IrcDotNet.IrcStream"/>
		/// </summary>
		/// <param name="value">The string to write to the <see cref="IrcDotNet.IrcStream"/>.</param>
		public void Write ( string value ) {
			if ( value == null )
				throw new ArgumenNullException("value");

			if ( this.autoCorrect && ( value.Length > 512 ) ) {
				this.CorrectWrite(value);
				return;
			}

			this.Write(this.encoding.GetBytes(value));
		}
		public void Write ( string format, params object[ ] args ) {
			if ( format == null )
				throw new ArgumenNullException("format");

			if ( args == null )
				throw new ArgumenNullException("args");

			this.Write(String.Format(format, args));
		}

		/// <summary>
		/// Writes data to the <see cref="IrcDotNet.IrcStream"/> with a carriage return
		/// and line feed appended to the end ("\r\n").
		/// </summary>
		/// <param name="value">The string to write to the <see cref="IrcDotNet.IrcStream"/>.</param>
		public void WriteLine ( string value ) {
			this.Write(buffer + "\r\n");
		}
		public void WriteLine ( string format, params object[ ] args ) {
			this.Write(( format + "\r\n" ), args);
		}

		#region IDisposable
		private bool disposed;

		protected override void Dispose ( bool disposing ) {
			if ( !this.disposed ) {
				try {
					if ( disposing ) {
						lock ( this.socket ) {
							this.socket.Shutdown(SocketShutdown.Both);
							this.socket.Close();
						}
					}

					this.disposed = true;
				}
				finally { base.Dispose(disposing); }
			}
		}
		#endregion
	}
}

